/*! ******************************************************************************
 *
 * Pentaho Data Integration
 *
 * Copyright (C) 2002-2017 by Hitachi Vantara : http://www.pentaho.com
 *
 *******************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 ******************************************************************************/

package org.pentaho.di.core.plugins;

import jdk.internal.loader.Resource;
import jdk.internal.loader.URLClassPath;
import jdk.internal.perf.PerfCounter;
import org.pentaho.di.i18n.BaseMessages;
import org.yzbdl.lanius.authorize.decrypt.jar.ClassDecrypt;

import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.net.URLClassLoader;
import java.security.*;
import java.util.*;
import java.util.jar.Attributes;
import java.util.jar.Attributes.Name;
import java.util.jar.JarFile;
import java.util.jar.Manifest;

@SuppressWarnings("restriction")
/**
 * 自定义URL类加载器
 *
 * @author yzbd@yzbdl.ac.cn
 */
public class KettleURLClassLoader extends URLClassLoader {
    /** for i18n purposes, needed by Translator2!! */
    private static Class<?> PKG = KettleURLClassLoader.class;

    private final URLClassPath ucp;
    private final AccessControlContext acc;

    private String name;

    public KettleURLClassLoader(URL[] urls, ClassLoader classLoader) {
        super(urls, classLoader);
        this.acc = AccessController.getContext();
        this.ucp = new URLClassPath(urls, acc);
    }

    public KettleURLClassLoader(URL[] url, ClassLoader classLoader, String name) {
        this(url, classLoader);
        this.name = name;
    }

    @Override
    public String toString() {
        return super.toString() + " : " + name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String getName() {
        return name;
    }

    @Override
    protected void addURL(URL url) {
        super.addURL(url);
    }

    protected Class<?> loadClassFromThisLoader(String className, boolean resolveClass) throws ClassNotFoundException {
        Class<?> clz = null;
        if ((clz = findLoadedClass(className)) != null) {
            if (resolveClass) {
                resolveClass(clz);
            }
            return clz;
        }

        if ((clz = findClass(className)) != null) {
            if (resolveClass) {
                resolveClass(clz);
            }
            return clz;
        }
        return clz;
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        final Class<?> result;
        try {
            result = AccessController.doPrivileged(new PrivilegedExceptionAction<Class<?>>() {
                @Override
				public Class<?> run() throws ClassNotFoundException {
                    String path = name.replace('.', '/').concat(".class");
                    Resource res = ucp.getResource(path, false);
                    if (res != null) {
                        try {
                            return defineClass(name, res);
                        } catch (IOException e) {
                            throw new ClassNotFoundException(name, e);
                        }
                    } else {
                        return null;
                    }
                }
            }, acc);
        } catch (java.security.PrivilegedActionException pae) {
            throw (ClassNotFoundException) pae.getException();
        }
        if (result == null) {
            throw new ClassNotFoundException(name);
        }
        return result;
    }

    /*
     * Defines a Class using the class bytes obtained from the specified
     * Resource. The resulting Class must be resolved before it can be
     * used.
     */
    private Class<?> defineClass(String name, Resource res) throws IOException {
        long t0 = System.nanoTime();
        int i = name.lastIndexOf('.');
        URL url = res.getCodeSourceURL();
        if (i != -1) {
            String pkgname = name.substring(0, i);
            // Check if package already loaded.
            Manifest man = res.getManifest();
            definePackageInternal(pkgname, man, url);
        }
        // Now read the class bytes and define the class
        java.nio.ByteBuffer bb = res.getByteBuffer();
        if (bb != null) {
            // Use (direct) ByteBuffer:
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, bb, cs);
        } else {
            byte[] b = res.getBytes();
            byte[] decryptedBytes = ClassDecrypt.decryptClassToBytes(b);
            // must read certificates AFTER reading bytes.
            CodeSigner[] signers = res.getCodeSigners();
            CodeSource cs = new CodeSource(url, signers);
            PerfCounter.getReadClassBytesTime().addElapsedTimeFrom(t0);
            return defineClass(name, decryptedBytes, 0, decryptedBytes.length, cs);
        }
    }

    //Also called by VM to define Package for classes loaded from the CDS
    // archive
    private void definePackageInternal(String pkgname, Manifest man, URL url) {
        if (getAndVerifyPackage(pkgname, man, url) == null) {
            try {
                if (man != null) {
                    definePackage(pkgname, man, url);
                } else {
                    definePackage(pkgname, null, null, null, null, null, null, null);
                }
            } catch (IllegalArgumentException iae) {
                // parallel-capable class loaders: re-verify in case of a
                // race condition
                if (getAndVerifyPackage(pkgname, man, url) == null) {
                    // Should never happen
                    throw new AssertionError("Cannot find package " +
                            pkgname);
                }
            }
        }
    }

    /*
     * Retrieve the package using the specified package name.
     * If non-null, verify the package using the specified code
     * source and manifest.
     */
    private Package getAndVerifyPackage(String pkgname,
                                        Manifest man, URL url) {
        Package pkg = getPackage(pkgname);
        if (pkg != null) {
            // Package found, so check package sealing.
            if (pkg.isSealed()) {
                // Verify that code source URL is the same.
                if (!pkg.isSealed(url)) {
                    throw new SecurityException(
                            "sealing violation: package " + pkgname + " is sealed");
                }
            } else {
                // Make sure we are not attempting to seal the package
                // at this code source URL.
                if ((man != null) && isSealed(pkgname, man)) {
                    throw new SecurityException(
                            "sealing violation: can't seal package " + pkgname +
                                    ": already loaded");
                }
            }
        }
        return pkg;
    }

    /*
     * Returns true if the specified package name is sealed according to the
     * given manifest.
     */
    private boolean isSealed(String name, Manifest man) {
        String path = name.replace('.', '/').concat("/");
        Attributes attr = man.getAttributes(path);
        String sealed = null;
        if (attr != null) {
            sealed = attr.getValue(Name.SEALED);
        }
        if (sealed == null) {
            if ((attr = man.getMainAttributes()) != null) {
                sealed = attr.getValue(Name.SEALED);
            }
        }
        return "true".equalsIgnoreCase(sealed);
    }

    protected Class<?> loadClassFromParent(String className, boolean arg1) throws ClassNotFoundException {
        Class<?> clz;
        if ((clz = getParent().loadClass(className)) != null) {
            if (arg1) {
                resolveClass(clz);
            }
            return clz;
        }
        throw new ClassNotFoundException("Could not find :" + className);
    }

    @Override
    protected synchronized Class<?> loadClass(String arg0, boolean arg1) throws ClassNotFoundException {
        try {
            return loadClassFromThisLoader(arg0, arg1);
        } catch (ClassNotFoundException e) {
            // ignore
        } catch (Exception e) {
            // ignore
        }

        return loadClassFromParent(arg0, arg1);
    }

    /*
     * Cglib doe's not creates custom class loader (to access package methotds
     * and classes ) it uses reflection to invoke "defineClass", but you can
     * call protected method in subclass without problems:
     */
    public Class<?> loadClass(String name, ProtectionDomain protectionDomain) {
        Class<?> loaded = findLoadedClass(name);
        if (loaded == null) {
            // Get the jar, load the bytes from the jar file, construct class
            // from scratch as in snippet below...

            /*
             *
             * loaded = super.findClass(name);
             *
             * URL url = super.findResource(newName);
             *
             * InputStream clis = getResourceAsStream(newName);
             */

            String newName = name.replace('.', '/');
            InputStream is = super.getResourceAsStream(newName);
            byte[] driverBytes = toBytes(is);
            byte[] decryptedBytes = ClassDecrypt.decryptClassToBytes(driverBytes);
            loaded = super.defineClass(name, decryptedBytes, 0, decryptedBytes.length, protectionDomain);

        }
        return loaded;
    }

    private byte[] toBytes(InputStream is) {
        byte[] retval = new byte[0];
        try {
            int a = is.available();
            while (a > 0) {
                byte[] buffer = new byte[a];
                is.read(buffer);

                byte[] newretval = new byte[retval.length + a];

                for (int i = 0; i < retval.length; i++) {
                    newretval[i] = retval[i]; // old part
                }
                for (int i = 0; i < a; i++) {
                    newretval[retval.length + i] = buffer[i]; // new part
                }

                retval = newretval;

                a = is.available(); // see what's left
            }
            return retval;
        } catch (Exception e) {
            System.out.println(
                    BaseMessages.getString(KettleURLClassLoader.PKG, "KettleURLClassLoader.Exception.UnableToReadClass") + e.toString());
            return null;
        }
    }

    private static Object getFieldObject(Class<?> clazz, String name, Object obj) throws Exception {
        Field field = clazz.getDeclaredField(name);
        field.setAccessible(true);
        return field.get(obj);
    }

    /**
     * This method is designed to clear out classloader file locks in windows.
     * clazzLdr class loader to clean up
     */
    public void closeClassLoader() {
        HashSet<String> closedFiles = new HashSet<String>();
        try {
            Object obj = KettleURLClassLoader.getFieldObject(URLClassLoader.class, "ucp", this);
            ArrayList<?> loaders = (ArrayList<?>) KettleURLClassLoader.getFieldObject(obj.getClass(), "loaders", obj);
            for (Object ldr : loaders) {
                try {
                    JarFile file = (JarFile) KettleURLClassLoader.getFieldObject(ldr.getClass(), "jar", ldr);
                    closedFiles.add(file.getName());
                    file.close();
                } catch (Exception e) {
                    // skip
                }
            }
        } catch (Exception e) {
            // skip
        }

        try {
            Vector<?> nativeLibArr = (Vector<?>) KettleURLClassLoader.getFieldObject(ClassLoader.class, "nativeLibraries", this);
            for (Object lib : nativeLibArr) {
                try {
                    Method fMethod = lib.getClass().getDeclaredMethod("finalize", new Class<?>[0]);
                    fMethod.setAccessible(true);
                    fMethod.invoke(lib, new Object[0]);
                } catch (Exception e) {
                    // skip
                }
            }
        } catch (Exception e) {
            // skip
        }

        HashMap<?, ?> uCache = null;
        HashMap<?, ?> fCache = null;

        try {
            Class<?> jarUrlConnClass = null;
            try {
                ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
                jarUrlConnClass = contextClassLoader.loadClass("sun.net.www.protocol.jar.JarURLConnection");
            } catch (Throwable skip) {
                // skip
            }
            if (jarUrlConnClass == null) {
                jarUrlConnClass = Class.forName("sun.net.www.protocol.jar.JarURLConnection");
            }
            Class<?> factory = KettleURLClassLoader.getFieldObject(jarUrlConnClass, "factory", null).getClass();
            try {
                fCache = (HashMap<?, ?>) KettleURLClassLoader.getFieldObject(factory, "fileCache", null);
            } catch (Exception e) {
                // skip
            }
            try {
                uCache = (HashMap<?, ?>) KettleURLClassLoader.getFieldObject(factory, "urlCache", null);
            } catch (Exception e) {
                // skip
            }
            if (uCache != null) {
                Set<?> set = null;
                while (set == null) {
                    try {
                        set = ((HashMap<?, ?>) uCache.clone()).keySet();
                    } catch (ConcurrentModificationException e) {
                        // Fix for BACKLOG-2149 - Do nothing - while loop will
                        // try again.
                    }
                }

                for (Object file : set) {
                    if (file instanceof JarFile) {
                        JarFile jar = (JarFile) file;
                        if (!closedFiles.contains(jar.getName())) {
                            continue;
                        }
                        try {
                            jar.close();
                        } catch (IOException e) {
                            // skip
                        }
                        if (fCache != null) {
                            fCache.remove(uCache.get(jar));
                        }
                        uCache.remove(jar);
                    }
                }
            } else if (fCache != null) {
                for (Object key : ((HashMap<?, ?>) fCache.clone()).keySet()) {
                    Object file = fCache.get(key);
                    if (file instanceof JarFile) {
                        JarFile jar = (JarFile) file;
                        if (!closedFiles.contains(jar.getName())) {
                            continue;
                        }
                        try {
                            jar.close();
                        } catch (IOException e) {
                            // ignore
                        }
                        fCache.remove(key);
                    }
                }
            }
            ucp.closeLoaders();
        } catch (Exception e) {
            // skip
            e.printStackTrace();
        }
    }

    @Override
    public URL getResource(String name) {
        URL url;
        url = findResource(name);
        if (url == null && getParent() != null) {
            url = getParent().getResource(name);
        }
        return url;
    }

}
